/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.flexengine.engine;

import com.flexengine.config.Configuration;
import com.flexengine.config.ConfigurationManager;
import com.flexengine.cursor.CursorManager;
import com.flexengine.cursor.GameCursor;
import com.flexengine.input.InputManager;
import com.flexengine.scene.GameScene;
import com.flexengine.scene.SceneManager;
import com.flexengine.sound.SoundManager;
import com.flexengine.utils.FontManager;
import com.flexengine.logger.Logger;
import com.flexengine.utils.ResourceManager;
import com.flexengine.utils.SpriteManager;
import com.flexengine.utils.TextureManager;
import com.flexengine.time.TimeManager;
import com.flexengine.language.LanguageManager;
import com.flexengine.view.GameWindow;
import com.flexengine.view.Renderer;
import com.flexengine.view.RendererType;
import com.flexengine.view.Texture;

/**
 * @author Luann R. Athayde
 * @version 1.0
 * @since 1.0
 */
public class FlexEngine implements Runnable {
    
    public static final String ENGINE_NAME      = "FlexEngine";
    public static final float  ENGINE_VERSION   = 1.0f;
    public static final char   ENGINE_BUILD     = 'b';
    public static final String ENGINE_FULL_NAME = ENGINE_NAME+" "+ENGINE_VERSION+ENGINE_BUILD;

    public static final int DEFAULT_UPS                 = 80;
    public static final int UNLIMITED_UPS               = 0;
    public static final int DEFAULT_NO_DELAYS_PER_YIELD = 16; // 16
    public static final int DEFAULT_MAX_FRAME_SKIPS     = 5;  // 5

    private String             gameName;
    
    private final GameWindow   window;
    private final Renderer     renderer;
    private final SceneManager sceneManager;
    
    private final FpsManager   fpsManager;
    private volatile boolean   showFps;
    
    private long             desireUPS;
    private long             desiredUpdateTime;
    private volatile boolean running;
    private volatile boolean paused;
    
    private volatile long    sleepTime;
    private volatile long    afterTime;
    private volatile long    beforeTime;
    private volatile long    overSleepTime;
    private volatile long    excessTime;
    private volatile long    noDelays;
    private volatile long    noDelaysPerYield;
    private volatile long    maxFrameSkips;

    private final GameCursor gameCursor;
    private Thread           tCore;
    
    private FlexEngine()    {
        Logger.logMessage(getClass().getSimpleName()+" - Creating engine...");
        this.gameName          = FlexEngine.ENGINE_FULL_NAME;
        this.window            = GameWindow.getInstance();
        this.window.setTitle(FlexEngine.ENGINE_FULL_NAME);
        this.renderer          = Renderer.getRenderer();
        // ---------------------------------------------------------------------
        this.sceneManager      = SceneManager.getInstance();
        this.fpsManager        = FpsManager.getInstance();
        this.showFps           = false;
        // ---------------------------------------------------------------------
        this.desireUPS         = DEFAULT_UPS;
        this.desiredUpdateTime = TimeManager.NANO_SECOND / this.desireUPS;
        this.running           = false;
        this.paused            = false;
        this.sleepTime         = 0L;
        this.afterTime         = 0L;
        this.beforeTime        = 0L;
        this.overSleepTime     = 0L;
        this.excessTime        = 0L;
        this.noDelays          = 0L;
        this.noDelaysPerYield  = DEFAULT_NO_DELAYS_PER_YIELD;
        this.maxFrameSkips     = DEFAULT_MAX_FRAME_SKIPS;
        // ---------------------------------------------------------------------
        this.gameCursor        = CursorManager.getGameCursor();
        // ---------------------------------------------------------------------
        this.tCore             = null;
        // ---------------------------------------------------------------------
        // -- Initialize the LanguageManager...
        LanguageManager.loadLanguages();
        // LanguageManager.setDefaultLanguage("English");
        // ---------------------------------------------------------------------
        // -- Initialize the TimeManager...
        TimeManager.getInstance();
        // ---------------------------------------------------------------------
        /**
         * Load the cursor textures...
         */
        TextureManager.getInstance().put("cursor",
                new Texture("cursor", "cursors/cursor.png"));
        TextureManager.getInstance().put("cursor2",
                new Texture("cursor2", "cursors/cursor2.png"));
        TextureManager.getInstance().put("cursor_type",
                new Texture("cursor_type", "cursors/cursor_type.png"));
        CursorManager.setDefaultTextureCursor(TextureManager.getInstance().get("cursor"));
        this.gameCursor.setTexture(TextureManager.getInstance().get("cursor"));
        CursorManager.hideBasicCursor();
        /**
         * Load all system available fonts...
         */
        FontManager.loadAllAvailableFonts();
        // ---------------------------------------------------------------------
        // -- Start InputManager...
        InputManager.create();
        // -- Load configuration...
        Logger.logMessage(getClass().getSimpleName()+" - Loading configuration...");
        Configuration config = ConfigurationManager.getInstace().loadConfiguration();
        if( config != null ) {
            ConfigurationManager.getInstace().setDefaultConfiguration(config);
        }
        applyConfiguration();
        // ---------------------------------------------------------------------
        Logger.logMessage(getClass().getSimpleName()+" - Engine created...");
        // ---------------------------------------------------------------------
    }
    private void applyConfiguration() {
        Logger.logMessage(getClass().getSimpleName()+" - configuration = "+ConfigurationManager.getInstace().getDefaultConfiguration());
        if( applyConfiguration(ConfigurationManager.getInstace().getDefaultConfiguration()) ) {
            Logger.logMessage(getClass().getSimpleName()+" applyConfiguration(): Configuration applied on engine...");
        } else {
            Configuration config = new Configuration();
            ConfigurationManager.getInstace().setDefaultConfiguration(config);
            ConfigurationManager.getInstace().saveConfiguration();
            applyConfiguration();
        }
    }
    public boolean applyConfiguration(Configuration config) {
        if( config!=null ) {
            GameWindow.getInstance().setSize(config.getResolution().getWidth(), config.getResolution().getHeight());
            GameWindow.getInstance().setDisplayMode(config.getResolution());
            setRenderer(config.getRenderer());
            LanguageManager.setDefaultLanguage(config.getLanguage());
            ResourceManager.setCurrentPath(config.getResourcesPath());
            SoundManager.getInstance().setEnable(config.isSound());
            SoundManager.getInstance().setMaxSimultaneousSounds(config.getChannels());
            //FlexEngineServer.getInstance().setServerPort(config.getPort());
            //FlexEngineServer.getInstance().setMaxUsers(config.getMaxConnections());
            return true;
        }
        return false;
    }

    // -- PUBLIC CORE METHODS
    public boolean start() {
    	if( running ) {
            Logger.logWarning(getClass().getSimpleName()+" - The engine is already running...");
            return false;
    	}
        Configuration config = ConfigurationManager.getInstace().getDefaultConfiguration();
        if( config!=null && config.isFullScreen() ) {
            GameWindow.getInstance().setSize(config.getResolution().getWidth(), config.getResolution().getHeight());
            GameWindow.getInstance().setFullScreen(true, GameWindow.DM_QUALITY);
        }
        // -- Initial parameters...
        this.running = true;
        // -- New core thread...
        tCore = new Thread(this,FlexEngine.ENGINE_FULL_NAME+" Core Thread");
        tCore.start();
        fpsManager.reset();
        fpsManager.start();
        TimeManager.getInstance().start();
        Logger.logMessage(getClass().getSimpleName()+": Start...");
        return true;
    }
    public void pause() {
        paused = !paused;
        for(int i=0;i<TimeManager.getTimers().size();i++) {
            TimeManager tm = TimeManager.getTimers().get(i);
            tm.pause(!tm.isPaused());
        }
        Logger.logMessage(getClass().getSimpleName()+": Paused = "+paused);
    }
    public void stop()  {
        Logger.logMessage(getClass().getSimpleName()+" - Stopping... ");
        fpsManager.stop();
        running = false;
        window.destroy();
        tCore = null;
        Logger.logMessage(getClass().getSimpleName()+" - Engine stopped and will close...");
        TextureManager.getInstance().clear();
        SoundManager.getInstance().clear();
        SpriteManager.getInstance().clear();
        System.gc();
        System.exit(0);
    }

    /**
     * Run the mains loops...
     */
    @Override
    public void run() {
        try {
            renderer.initRender();
            coreLoop();
        } catch(Exception e) {
            Logger.logError(getClass().getSimpleName()+": "+e.toString());
            //e.printStackTrace();
        } finally {
            running = false;
        }
    }

    private synchronized void coreLoop() {
        while (running) {
            try {
                beforeTime = TimeManager.nanoTime();
                skipFramesInExcessTime();
                // Updates, renders and paint the screen
                if( !toFullScreen ) {
                    coreUpdate();
                    // -- Render all the components on the backBuffer...
                    //renderer.render();
                    // -- Show...
                    if( renderer.render() && renderer.show() ) {
                        fpsManager.incrementFPS();
                    }
                }
                afterTime = TimeManager.nanoTime();
                sleepTime = calculateSleepTime();
                if (sleepTime >= 0 && desireUPS > 0 ) {
                    sleep(sleepTime);
                } else if( desireUPS == 0 ) {
                    sleep(1000);
                } else if( sleepTime<0 && desireUPS > 0 ) {
                    excessTime -= sleepTime; // Sleep time is negative
                    overSleepTime = 0L;
                    yieldIfNeed();
                }
            } catch(InterruptedException e) {
                Logger.logError(getClass().getSimpleName()+" coreLoop(): "+e);
                System.exit(-1);
            }
        }
    }
    
    // -- Update
    private synchronized void coreUpdate()   {
        // -- Update the renderer...
        if( renderer!=null ) {
            renderer.update();
        }
        // -- paused...
        if( paused || !running ) {
            return;
        }
        // -- Update the states of the game...
        GameScene scene = sceneManager.getCurrentScene();
        if( scene!=null ) {
            long passTime = (afterTime - beforeTime) + overSleepTime;
            if( passTime<0 ) {
                passTime = 0;
            }
            scene.update( passTime , TimeManager.getInstance().getTotalTime());
            this.fpsManager.incrementUPS();
        }
    }
    
    /**
     * Sleep the given amount of time. Since the sleep() method of the thread
     * class is not precise, the overSleepTime will be calculated.
     *
     * @param nanos Number of nanoseconds to sleep.
     * @throws InterruptedException If the thread was interrupted
     */
    private void sleep(long nanos) throws InterruptedException {
        noDelays = 0;
        long beforeSleep = TimeManager.nanoTime();
        Thread.sleep(nanos / TimeManager.MILISECOND_IN_NANO, 
                (int) (nanos % TimeManager.MILISECOND_IN_NANO));
        overSleepTime = TimeManager.nanoTime() - beforeSleep - nanos;
    }

    /**
     * If the number of frames without a delay is reached, force the thread to
     * yield, allowing other threads to process.
     */
    private void yieldIfNeed() {
        if (++noDelays == noDelaysPerYield) {
            Thread.yield();
            noDelays = 0;
            //Logger.logWarning(getClass().getSimpleName()+" - Engine YIELD!!! Check the current application...");
        }
    }
    
    // -- Skip some frames...
    private void skipFramesInExcessTime() {
        int skips = 0;
        while ((excessTime > desiredUpdateTime) && (skips < maxFrameSkips)) {
            excessTime -= desiredUpdateTime;
            coreUpdate();
            skips++;
        }
        fpsManager.addSkips(skips);
    }
    
    /**
     * Calculates the sleep time based in the calculation the previous loop. To
     * achieve this time, the frame display time will be subtracted by the time
     * elapsed in the last computation (afterTime - beforeTime). Then, if in the
     * previous loop there was an oversleep time, this will also be subtracted,
     * so the system can compensate this overtime.
     */
    private long calculateSleepTime()
    {
        return desiredUpdateTime - (afterTime - beforeTime) - overSleepTime;
    }
    
    public long timePass() {
        return (afterTime - beforeTime)+sleepTime;
    }

    private volatile boolean toFullScreen = false;
    public synchronized void fullScreenStateChanging() {
        toFullScreen = !toFullScreen;
    }
    
    /*
     * Gets/Sets of this class...
     */
    public FpsManager getFpsManager() {
        return fpsManager;
    }
    public SceneManager getSceneManager() {
        return sceneManager;
    }
    public boolean isRunning() {
        return running;
    }
    public boolean isPaused() {
        return paused;
    }
    public GameCursor getGameCursor() {
        return gameCursor;
    }
    public boolean isShowFps() {
        return showFps;
    }
    public FlexEngine setShowFps(boolean showFps) {
        this.showFps = showFps;
        return this;
    }
    public float getAvarageFPS() {
        if( fpsManager.getTotalFps()>0 && TimeManager.getInstance().getTotalTime()>0 ) {
            return ((fpsManager.getTotalFps())/(TimeManager.getInstance().getTotalTime()/1000f));
        }
        return 0f;
    }
    public FlexEngine setGameName(String gameName) {
        this.gameName = gameName;
        if( window != null ) {
            window.setTitle(gameName);
        }
        return this;
    }
    public String getGameName() {
        return gameName;
    }
    public FlexEngine setDesireUPS(long desireUPS) {
        this.desireUPS = desireUPS;
        if( desireUPS > 0 ) {
            this.desiredUpdateTime = TimeManager.NANO_SECOND / this.desireUPS;
        }
        else if(desireUPS == 0 ) {
            this.desiredUpdateTime = 0;
            this.desireUPS = 0;
        }
        else {
            this.desireUPS = DEFAULT_UPS;
        }
        return this;
    }
    public long getDesireUPS() {
        return desireUPS;
    }
    public void setMaxFrameSkips(long maxFrameSkips) {
        this.maxFrameSkips = maxFrameSkips;
    }
    public long getMaxFrameSkips() {
        return maxFrameSkips;
    }
    public long getNoDelaysPerYield() {
        return noDelaysPerYield;
    }
    public void setNoDelaysPerYield(long noDelaysPerYield) {
        this.noDelaysPerYield = noDelaysPerYield;
    }

    public void setRenderer(RendererType render) {
        renderer.changeRender(render);
        Logger.logMessage(getClass().getSimpleName()+" - setRenderer(): renderer = "+renderer);
    }

    public Renderer getRenderer() {
        return renderer;
    }
    
    public static String getEnviroment() {
        String env = System.getProperty("os.name")+" "
                + System.getProperty("os.version")+" "
                + System.getProperty("os.arch");
        env+= " in "
                + System.getProperty("java.vm.name")+" "
                + System.getProperty("java.version");
        return env;
    }
    
    private static FlexEngine instance;
    public synchronized static FlexEngine getInstance() {
        if( instance == null ) {
            instance = new FlexEngine();
        }
        return instance;
    }
}